昨天,我們完成了行程管家小助手的前端架構基礎,定義了 Message 與 ChatRequest 等資料模型,為互動邏輯鋪好了第一塊地基。
而今天,我們要邁出關鍵一步 —— 讓行程管家小助手真正與使用者互動!
也就是說,當使用者輸入訊息後,系統不僅要能回覆,還要能根據回應內容觸發對應的地圖動作,形成「前端 ↔ 後端 ↔ 使用者」的完整溝通橋梁。
今日目標
MessageAdapter.java的視窗畫面程式碼
package com.example.ittext.ui.chat;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import com.example.ittext.R;
import java.util.List;
public class MessageAdapter extends RecyclerView.Adapter<MessageAdapter.MessageViewHolder> {
private List<Message> messageList;
private OnActionClickListener actionListener;
public interface OnActionClickListener {
void onMapActionClick(MessageAction action, Object data);
}
public MessageAdapter(List<Message> messageList, OnActionClickListener listener) {
this.messageList = messageList;
this.actionListener = listener;
}
@NonNull
@Override
public MessageViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.chat_message_item, parent, false);
return new MessageViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull MessageViewHolder holder, int position) {
Message message = messageList.get(position);
holder.messageText.setText(message.getContent());
if (message.isUser()){
holder.messageText.setBackgroundResource(R.drawable.bg_message_sent);
}else {
holder.messageText.setBackgroundResource(R.drawable.bg_message_received);
}
// 如果訊息包含地圖動作,顯示按鈕
if (message.getAction() != MessageAction.NONE) {
holder.actionButton.setVisibility(View.VISIBLE);
holder.actionButton.setText(getActionButton(message.getAction()));
holder.actionButton.setVisibility(View.VISIBLE);
holder.itemView.setOnClickListener(v -> {
if (actionListener != null) {
actionListener.onMapActionClick(message.getAction(), message.getActionData());
}
});
} else {
holder.itemView.setVisibility(View.GONE);
}
}
private String getActionButton(MessageAction action) {
switch (action) {
case OPEN_MAP: return "查看地圖";
case SHOW_ROUTE: return "查看路線";
case SHOW_LOCATION: return "查看景點";
default: return "";
}
}
public static class MessageViewHolder extends RecyclerView.ViewHolder {
TextView messageText;
Button actionButton;
public MessageViewHolder(@NonNull View itemView) {
super(itemView);
messageText = itemView.findViewById(R.id.message_text);
actionButton = itemView.findViewById(R.id.action_button);
}
}
@Override
public int getItemCount() {
return messageList != null ? messageList.size() : 0;
}
}
chatAPI的程式碼
private void getChat(String message) {
ChatRequest chatRequest = new ChatRequest(message);
Log.d("=== 發送請求 ===" , "訊息: " + message);
getApi.getChat(chatRequest)
.subscribeOn(io.reactivex.rxjava3.schedulers.Schedulers.io())
.observeOn(io.reactivex.rxjava3.android.schedulers.AndroidSchedulers.mainThread())
.subscribe(new io.reactivex.rxjava3.core.Observer<ChatResponse>() {
@Override
public void onSubscribe(@NonNull Disposable d) {
compositeDisposable.add(d);
}
@Override
public void onNext(@NonNull ChatResponse chatResponse) {
Log.d("=== API 回應成功 ===", "完整回應: " + (chatResponse != null ? chatResponse.toString() : "null"));
if (chatResponse == null || chatResponse.getReply() == null || chatResponse.getReply().isEmpty()) {
Log.e("ChatActivity", "Error: Reply is null or empty, Response: " + (chatResponse != null ? chatResponse.toString() : "null"));
messageList.add(new Message("抱歉,我沒有收到有效的回應。", false));
messageAdapter.notifyItemInserted(messageList.size() - 1);
ChatRecyclerView.smoothScrollToPosition(messageList.size() - 1);
return;
}
Message aiMessage = generateAIResponse(chatResponse);
messageList.add(aiMessage);
messageAdapter.notifyItemInserted(messageList.size() - 1);
ChatRecyclerView.smoothScrollToPosition(messageList.size() - 1);
// 如果 AI 回覆包含地圖動作,則觸發地圖功能
if (aiMessage.getAction() != null) {
handleMapAction(aiMessage.getAction(), aiMessage.getActionData());
}
}
@Override
public void onError(Throwable e) {
// 顯示錯誤訊息
Log.d("=== API 錯誤 ===", "Error type: " + e.getClass().getSimpleName());
Log.d("=== API 錯誤 ===", "Error message: " + e.getMessage());
if (e instanceof HttpException) {
HttpException httpException = (HttpException) e;
int code = httpException.code();
Log.d("TAG", "HTTP Status Code: " + code);
try {
String errorBody = httpException.response().errorBody().string();
Log.d("TAG", "Error Body: " + errorBody);
} catch (IOException ioException) {
Log.d( "Cannot read error body","ioException", ioException);
}
} else if (e instanceof IOException) {
Log.d("TAG", "Network Error: " + e.getMessage());
} else {
Log.d("TAG", "Unexpected Error: " + e.getMessage());
}
Toast.makeText(ChatActivity.this, "訊息發送失敗,請稍後再試", Toast.LENGTH_SHORT).show();
messageList.add(new Message("抱歉,系統發生錯誤,請稍後再試。", false));
messageAdapter.notifyItemInserted(messageList.size() - 1);
ChatRecyclerView.smoothScrollToPosition(messageList.size() - 1);
}
@Override
public void onComplete() {
Log.d("=== API 請求完成 ===", "onComplete called");
}
});
}
private Message generateAIResponse(ChatResponse chatResponse ) {
String replyText = chatResponse.getReply();
// 景點推薦
if (chatResponse.getAction() != null && !chatResponse.getAction().isEmpty()) {
Log.d("TAG", "Response has action: " + chatResponse.getAction());
switch (chatResponse.getAction()) {
case "SHOW_LOCATION":
return new Message(
replyText,
false,
MessageAction.SHOW_LOCATION,
chatResponse.getPlaces()
);
case "SHOW_ROUTE":
RouteData routeData = new RouteData(
chatResponse.getStartPoint(),
chatResponse.getEndPoint(),
chatResponse.getWaypoints()
);
return new Message(
replyText,
false,
MessageAction.SHOW_ROUTE,
routeData
);
case "OPEN_MAP":
return new Message(
replyText,
false,
MessageAction.OPEN_MAP,
null
);
}
}
// 一般文字回應
return new Message(replyText, false);
}
// 處理地圖動作
private void handleMapAction(MessageAction action, Object data) {
Intent intent = new Intent(this, MapsActivity.class);
switch (action) {
case SHOW_LOCATION:
intent.putExtra("action", "show_locations");
if (data instanceof List) {
intent.putStringArrayListExtra("places", new ArrayList<>((List<String>) data));
}
break;
case SHOW_ROUTE:
intent.putExtra("action", "show_route");
if (data instanceof RouteData) {
RouteData route = (RouteData) data;
intent.putExtra("start", route.start);
intent.putExtra("end", route.end);
intent.putStringArrayListExtra("waypoints", new ArrayList<>(route.waypoints));
}
break;
case OPEN_MAP:
intent.putExtra("action", "open_map");
break;
}
startActivity(intent);
}
// 路線數據類別
static class RouteData {
String start;
String end;
List<String> waypoints;
RouteData(String start, String end, List<String> waypoints) {
this.start = start;
this.end = end;
this.waypoints = waypoints;
}
}